package com.comboo.example.web.aop;

import com.comboo.example.web.api.RequestWrapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author Chen, Zhuhui
 */
@Aspect
public class HandlerLogger {

    private static final Logger logger = LoggerFactory.getLogger(HandlerLogger.class.getSimpleName());
    private static final ConcurrentHashMap<Class<?>, ArgumentDumpingFunction> ARG_DUMPING_FUNCS = new ConcurrentHashMap<>();
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    static {
        OBJECT_MAPPER.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    }

    @Pointcut("@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.web.bind.annotation.RestController)")
    private void withinController() {
    }

    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.DeleteMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.PatchMapping) || " +
            "@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    private void controllerMethods() {
    }

    @Around("withinController() && controllerMethods()")
    public Object dumpControllerArguments(ProceedingJoinPoint pjp) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        if (request == null) {
            // 非请求类型的调用（例如直接调用controller方法），不进行参数信息提取。
            return pjp.proceed();
        }

        try {
            dumpRequestToLog(request, pjp);
        } catch (Exception ex) {
            logger.warn("日志切面出现问题，导致无法正常记录日志。请检查异常（见下文）。本异常已被捕获，不会影响方法本身的调用", ex);
        }

        return pjp.proceed();
    }

    private void dumpRequestToLog(HttpServletRequest request, ProceedingJoinPoint pjp) throws Exception {
        if (logger.isDebugEnabled()) {
            logger.debug("[请求内容：\n" +
                            "客户端地址：{}\n" +
                            "地址：{} {}{}\n" +
                            "Headers：\n" +
                            "{}\n" +
                            "处理方法：{}\n" +
                            "解析后的参数：\n" +
                            "{}]",
                    dumpClientAddress(request),
                    dumpRequestMethod(request),
                    dumpUrl(request),
                    dumpQuery(request),
                    dumpHeaders(request),
                    dumpHandlerMethod(pjp),
                    dumpHandlerMethodArgs(pjp));
        }
    }

    private String dumpClientAddress(HttpServletRequest request) {
        return request.getRemoteAddr();
    }

    private String dumpRequestMethod(HttpServletRequest request) {
        String requestMethod = request.getMethod();
        if (requestMethod == null) {
            // 不太可能的情况
            return "GET";
        } else {
            return requestMethod.toUpperCase();
        }
    }

    // 只导出可以导出的参数
    private boolean canDumpArgument(Object arg) {
        return arg instanceof RequestWrapper;
    }

    private String dumpHandlerMethodArgs(ProceedingJoinPoint pjp) throws Exception {
        Object[] args = pjp.getArgs();
        StringBuilder sb = new StringBuilder();
        int cnt = 0;
        for (Object arg : args) {
            sb.append("参数").append(cnt).append("：");

            if (arg == null) {
                sb.append("<null>");
            } else if (canDumpArgument(arg)) {
                Class<?> argClazz = arg.getClass();
                if (ARG_DUMPING_FUNCS.containsKey(argClazz)) {
                    sb.append(ARG_DUMPING_FUNCS.get(argClazz).dump(arg));
                } else {
                    ArgumentDumpingFunction func = buildArgDumpingFunc(arg);
                    sb.append(func.dump(arg));
                }
            } else {
                sb.append(arg.toString());
            }

            sb.append('\n');
            ++cnt;
        }
        return sb.toString();
    }

    private String dumpHandlerMethod(ProceedingJoinPoint pjp) {
        return pjp.getSignature().toLongString();
    }

    private String dumpQuery(HttpServletRequest request) {
        String queryString = request.getQueryString();
        queryString = queryString == null ? "" : ("?" + queryString);
        queryString = queryString.replaceAll("access_token=[^&]+", "access_token=********");
        return queryString;
    }

    private String dumpUrl(HttpServletRequest request) {
        return request.getRequestURI();
    }

    private String dumpHeaders(HttpServletRequest request) {
        StringBuilder sb = new StringBuilder();
        Enumeration<String> allHeaders = request.getHeaderNames();
        if (allHeaders != null) {
            while (allHeaders.hasMoreElements()) {
                String header = allHeaders.nextElement();
                // 不记录敏感信息
                if ("cookie".equalsIgnoreCase(header)) {
                    sb.append("COOKIE: ********\n");
                } else if ("authorization".equalsIgnoreCase(header)) {
                    sb.append("AUTHORIZATION: ********\n");
                } else {
                    String headerValue = request.getHeader(header);
                    sb.append(header.toUpperCase()).append(": ").append(headerValue);
                    if (allHeaders.hasMoreElements()) {
                        sb.append('\n');
                    }
                }
            }
        }
        return sb.toString();
    }

    private ArgumentDumpingFunction buildArgDumpingFunc(Object arg) {
        // 参数提取的原则是如果参数对象的类有直接复写 toString() ，那么就调用 toString() 来作为最终的
        // 提取内容；如果没有直接复写 toString() ，那么转成json输出
        try {
            if (arg instanceof RequestWrapper) {
                // 特殊处理下ApiRequest
                Object payload = ((RequestWrapper) arg).getPayload();
                if (payload != null) {
                    payload.getClass().getDeclaredMethod("toString");
                    ArgumentDumpingFunction func = Object::toString;
                    ARG_DUMPING_FUNCS.put(arg.getClass(), func);
                    return func;
                }
            } else {
                arg.getClass().getDeclaredMethod("toString");
                ArgumentDumpingFunction func = Object::toString;
                ARG_DUMPING_FUNCS.put(arg.getClass(), Object::toString);
                return func;
            }
        } catch (NoSuchMethodException e) {
            // 没有复写toString的情况，转成json输出
        }

        ArgumentDumpingFunction func = OBJECT_MAPPER::writeValueAsString;
        ARG_DUMPING_FUNCS.put(arg.getClass(), func);
        return func;
    }

    interface ArgumentDumpingFunction {
        String dump(Object arg) throws Exception;
    }

}
